package com.tangyb.spring.boot.excel.util;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.ss.util.CellRangeAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Excel导入工具类
 *
 * @author tangyb
 */
public class ImportExcelUtil {
    private static final Logger log = LoggerFactory.getLogger(ImportExcelUtil.class);

    /**
     * 日期格式化
     */
    private final static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

    /**
     * 数值格式化字符串
     */
    private final static DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0");

    /**
     * <pre>
     * excel日期dateFormat数组，日期格式可以通过getDataFormat()值来判断
     * yyyy-MM-dd-----14
     * yyyy年m月d日---  31
     * yyyy年m月-------57
     * m月d日  ----------58
     * HH:mm-----------20
     * h时mm分  -------32
     * </pre>
     */
    private static final short[] DATE_FORMAT_TYPES = new short[]{14, 20, 31, 32, 57, 58};

    /**
     * 读取Excel所有sheet中的数据
     *
     * @param file   文件
     * @param rowNum 内容初始行号[Excel行号以0开始]
     * @return 结果集合
     * @throws IOException IO异常
     */
    public static List<List<String>> readExcel(MultipartFile file, int rowNum) throws IOException {
        if (file == null || file.isEmpty()) {
            return null;
        }

        // InputStream输入流
        InputStream is = file.getInputStream();
        // 原始文件名
        String fileName = file.getOriginalFilename();

        return readExcel(is, fileName, rowNum, false);
    }

    /**
     * 读取Excel第一个sheet中的数据
     *
     * @param file   文件
     * @param rowNum 内容初始行号[Excel行号以0开始]
     * @return 结果集合
     * @throws IOException IO异常
     */
    public static List<List<String>> readExcelSheetOne(MultipartFile file, int rowNum) throws IOException {
        if (file == null || file.isEmpty()) {
            return null;
        }

        // InputStream输入流
        InputStream in = file.getInputStream();
        // 原始文件名
        String fileName = file.getOriginalFilename();

        return readExcel(in, fileName, rowNum, true);
    }

    /**
     * 读取Excel所有sheet中的数据
     *
     * @param in       InputStream输入流
     * @param fileName 文件名称
     * @param rowNum   内容初始行号[Excel行号以0开始]
     * @return 结果集合
     * @throws IOException IO异常
     */
    public static List<List<String>> readExcel(InputStream in, String fileName, int rowNum) throws IOException {
        return readExcel(in, fileName, rowNum, false);
    }

    /**
     * 读取Excel第一个sheet中的数据
     *
     * @param in       InputStream输入流
     * @param fileName 文件名称
     * @param rowNum   内容初始行号[Excel行号以0开始]
     * @return 结果集合
     * @throws IOException IO异常
     */
    public static List<List<String>> readExcelSheetOne(InputStream in, String fileName, int rowNum) throws IOException {
        return readExcel(in, fileName, rowNum, true);
    }

    /**
     * 读取Excel中的数据
     *
     * @param in       InputStream输入流
     * @param fileName 文件名称
     * @param rowNum   内容初始行号[Excel行号以0开始]
     * @param sheetOne 是否只读取第一个sheet
     * @return 结果集合
     * @throws IOException IO异常
     */
    private static List<List<String>> readExcel(InputStream in, String fileName, int rowNum, boolean sheetOne) throws IOException {
        log.info("解析Excel开始，文件名称：{}，内容初始行号：{}，是否只读取第一个sheet：{}", fileName, rowNum, sheetOne);
        if (in == null) {
            return null;
        }

        // Excel所有数据集合
        List<List<String>> workList = new ArrayList<>();

        // 创建Excel工作薄
        Workbook work = getWorkbook(in);

        // sheet数量
        int sheetNum = work.getNumberOfSheets();

        // 判断是否只读取第一个sheet中的数据
        if (sheetOne) {
            sheetNum = 1;
        }

        // 遍历Excel中所有的sheet
        for (int i = 0; i < sheetNum; i++) {
            // 获取Sheet工作表
            Sheet sheet = work.getSheetAt(i);
            if (sheet == null) {
                log.info("Excel文件：{}，第[{}]工作表数据为空", fileName, i + 1);
                continue;
            }

            // 工作表名称
            String sheetName = sheet.getSheetName();

            // 初始行
            int initRowNum = sheet.getFirstRowNum();

            // 当前sheet中的所有行
            int sRowNum = sheet.getLastRowNum();

            // 循环读内容开始行
            initRowNum = initRowNum + rowNum;

            log.info("Excel文件：{}，工作表：{}，总行数：{}，解析初始行：{}，开始解析", fileName, sheetName, sRowNum + 1, initRowNum);

            // 遍历当前sheet中的所有行
            for (int rowIx = initRowNum; rowIx <= sRowNum; rowIx++) {
                Row row = sheet.getRow(rowIx);
                // 过滤空行
                if (isEmptyRow(row)) {
                    log.info("Excel文件：{}，工作表：{}，行：{}，数据为空", fileName, sheetName, rowIx + 1);
                    continue;
                }

                // 获取第一个单元格的编号，如果行不包含任何单元格，则为-1
                short minColIx = row.getFirstCellNum();
                // 获取最后一个单元格的索引加一
                short maxColIx = row.getLastCellNum();

                // 行数据
                List<String> rowList = new ArrayList<>();
                // 遍历行中的所有列
                for (short colIx = minColIx; colIx < maxColIx; colIx++) {
                    // 获取单元格
                    Cell cell = row.getCell(colIx);
                    // 判断是否是合并单元格
                    if (isMergedRegion(sheet, rowIx, colIx)) {
                        // 获取合并单元格的内容
                        rowList.add(getMergedRegionValue(sheet, rowIx, colIx));
                    } else {
                        // 对表格中数值进行格式化
                        rowList.add(getCellValue(cell));
                    }
                }
                workList.add(rowList);
            }
        }

        // 关闭读取工作簿
        work.close();

        log.info("解析Excel结束");

        return workList;
    }

    /**
     * 读取Excel中的数据
     *
     * @param in       InputStream输入流
     * @param fileName 文件名称
     * @param rowNum   内容初始行号[Excel行号以0开始]
     * @param sheetOne 是否只读取第一个sheet
     * @return 结果集合
     * @throws IOException IO异常
     */
    private static List<List<String>> readExcelIterator(InputStream in, String fileName, int rowNum, boolean sheetOne) throws IOException {
        log.info("解析Excel开始，文件名称：{}，内容初始行号：{}，是否只读取第一个sheet：{}", fileName, rowNum, sheetOne);
        if (in == null) {
            return null;
        }

        // Excel所有数据集合
        List<List<String>> workList = new ArrayList<>();

        // 创建Excel工作薄
        Workbook work = getWorkbook(in);

        // sheet数量
        int sheetNum = work.getNumberOfSheets();

        // 判断是否只读取第一个sheet中的数据
        if (sheetOne) {
            sheetNum = 1;
        }

        // 遍历Excel中所有的sheet
        for (int i = 0; i < sheetNum; i++) {
            // 获取Sheet工作表
            Sheet sheet = work.getSheetAt(i);
            if (sheet == null) {
                log.info("Excel文件：{}，第[{}]工作表数据为空", fileName, i + 1);
                continue;
            }

            // 返回物理行的迭代器
            Iterator<Row> rowIterator = sheet.rowIterator();

            int rowIndex = 0;
            while (rowIterator.hasNext()) {
                // 获取行
                Row row = rowIterator.next();

                // 非内容初始行不处理
                if(rowIndex < rowNum) {
                    rowIndex++;
                    continue;
                }

                // 获取行的所有列迭代器
                Iterator<Cell> cellIterator = row.cellIterator();

                // 存储行数据
                List<String> rowList = new ArrayList<>();
                // 遍历一行的列
                while (cellIterator.hasNext()) {
                    Cell cell = cellIterator.next();
                    // 对表格中数值进行格式化
                    rowList.add(getCellValue(cell));
                }
                workList.add(rowList);
            }
        }

        // 关闭读取工作簿
        work.close();

        log.info("解析Excel结束");

        return workList;
    }

    /**
     * 根据文件后缀，自适应上传文件的版本
     *
     * @param is       输入流
     * @return Excel工作簿
     * @throws IOException IO异常
     */
    private static Workbook getWorkbook(InputStream is) throws IOException {
        return WorkbookFactory.create(is);
    }

    /**
     * 获取单元格中的数值
     *
     * @param cell 单元格
     * @return 数值
     */
    private static String getCellValue(Cell cell) {
        if (cell == null) {
            return "";
        }

        // 单元格值
        String value;

        // 单元格类型
        CellType cellTypeEnum = cell.getCellType();
        // 根据单元格类型获取值并格式化为字符串
        switch (cellTypeEnum) {
            // 字符串
            case STRING:
                value = trimCellValue(cell.getStringCellValue());
                break;
            // 数值单元格类型 (整数、小数、日期)
            case NUMERIC:
                // 检查单元格是否包含日期
                if (isExcelDate(cell)) {
                    value = DATE_FORMAT.format(cell.getDateCellValue());
                }
                // 数字类型
                else {
                    value = DECIMAL_FORMAT.format(cell.getNumericCellValue());
                }
                break;
            // 布尔类型
            case BOOLEAN:
                value = String.valueOf(cell.getBooleanCellValue());
                break;
            // 错误类型
            case ERROR:
                // 空白
            case BLANK:
                // 其他
            default:
                value = "";
        }

        return value;
    }

    /**
     * 判断单元格是否为Excel日期
     *
     * @param cell 单元格
     * @return 是否
     */
    public static boolean isExcelDate(Cell cell) {
        if (cell == null) {
            return false;
        }
        if (CellType.NUMERIC == cell.getCellType()) {
            if (DateUtil.isCellDateFormatted(cell)) {
                return true;
            }
            //根据getDataFormat()值来判断是否为日期
            short format = cell.getCellStyle().getDataFormat();
            for (short i : DATE_FORMAT_TYPES) {
                if (format == i) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 检查行是否为为空或该行中的所有单元格均为空
     *
     * @param row 行
     * @return 是否
     */
    private static boolean isEmptyRow(Row row) {
        if (row == null || row.toString().isEmpty()) {
            return true;
        } else {
            Iterator<Cell> it = row.iterator();
            boolean isEmpty = true;
            // 遍历所有单元格
            while (it.hasNext()) {
                Cell cell = it.next();
                // 只要有非空单元格为有数据
                if (cell.getCellType() != CellType.BLANK) {
                    isEmpty = false;
                    break;
                }
            }
            return isEmpty;
        }
    }

    /**
     * 获取合并单元格的内容
     *
     * @param sheet  Excel工作表
     * @param row    行号
     * @param column 列号
     * @return 内容
     */
    private static String getMergedRegionValue(Sheet sheet, int row, int column) {
        int sheetMergeCount = sheet.getNumMergedRegions();
        for (int i = 0; i < sheetMergeCount; i++) {
            // 返回指定索引处的合并区域
            CellRangeAddress ca = sheet.getMergedRegion(i);
            // 左上角的列号
            int firstColumn = ca.getFirstColumn();
            // 右下角的列号
            int lastColumn = ca.getLastColumn();
            // 左上角的行号
            int firstRow = ca.getFirstRow();
            // 右下角的行号
            int lastRow = ca.getLastRow();

            // 合并单元格判断，行对比
            if (row >= firstRow && row <= lastRow) {
                // 合并单元格判断，列对比
                if (column >= firstColumn && column <= lastColumn) {
                    Row fRow = sheet.getRow(firstRow);
                    Cell fCell = fRow.getCell(firstColumn);
                    return getCellValue(fCell);
                }
            }
        }
        return "";
    }

    /**
     * 判断是否是合并单元格
     *
     * @param sheet  Excel工作表
     * @param row    行号
     * @param column 列号
     * @return 是否
     */
    private static boolean isMergedRegion(Sheet sheet, int row, int column) {
        int sheetMergeCount = sheet.getNumMergedRegions();
        for (int i = 0; i < sheetMergeCount; i++) {
            // 返回指定索引处的合并区域
            CellRangeAddress ca = sheet.getMergedRegion(i);
            // 左上角的列号
            int firstColumn = ca.getFirstColumn();
            // 右下角的列号
            int lastColumn = ca.getLastColumn();
            // 左上角的行号
            int firstRow = ca.getFirstRow();
            // 右下角的行号
            int lastRow = ca.getLastRow();
            // 合并单元格判断，行对比
            if (row >= firstRow && row <= lastRow) {
                // 合并单元格判断，列对比
                if (column >= firstColumn && column <= lastColumn) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 加工单元格值
     *
     * @param cellValue 单元格值
     * @return 结果
     */
    private static String trimCellValue(String cellValue) {
        if (cellValue == null) {
            cellValue = "";
        } else {
            cellValue = trimC(cellValue).replace("\r", "").replace("\n", "");
        }
        return cellValue;
    }

    /**
     * 删除前后空格
     *
     * @param value 值
     * @return 结果
     */
    private static String trimC(String value) {
        int len = value.length();
        int st = 0;
        char[] val = value.toCharArray();
        while ((st < len) && ((val[st] == ' ') || (val[st] <= ' '))) {
            st++;
        }
        while ((st < len) && ((val[(len - 1)] == ' ') || (val[(len - 1)] <= ' '))) {
            len--;
        }
        return (st > 0) || (len < value.length()) ? value.substring(st, len) : value;
    }
}
